pub mod his;
pub mod sse;
pub mod sync;
pub mod syns;
pub mod qbt;
pub mod qtr;

use anyhow::anyhow;
use anyhow::Result;
use std::str::FromStr;
use std::sync::Arc;
use chrono::DateTime;
use chrono::Utc;
use chrono_tz::Tz;
use cron::Schedule;
use log::info;
use tokio::signal;
use tokio::sync::mpsc;
use tokio::sync::Mutex;

/// [schedule]
///
/// gen_todos_next_time = "0 33 15,17,22 * * *"
///
/// crawl_next_time = "0 0/1 * * * *"
#[derive(Debug)]
pub(crate) struct CommandConfig {
    gen_todos_next_time: String,
    crawl_next_time: String,
    kline_his_max_days: i64,
    kline_his_default_days: i64,
}

impl CommandConfig {
    fn from_toml(config: &toml::Value) -> Result<Self> {
        Ok(CommandConfig {
            gen_todos_next_time: config["schedule"]["gen_todos_next_time"]
                .as_str()
                .ok_or("Missing schedule gen_todos_next_time")
                .unwrap_or_default()
                .to_owned(),

            crawl_next_time: config["schedule"]["crawl_next_time"]
                .as_str()
                .ok_or("Missing schedule crawl_next_time")
                .map_err(|e| anyhow!(e))
                .unwrap_or_default()
                .to_owned(),

            kline_his_max_days: config["kline_his"]["max_days"]
                .as_integer()
                .ok_or("Missing kline_his max_days")
                .unwrap_or_else(|_| 2500i64)
                .to_owned(),

            kline_his_default_days: config["kline_his"]["default_days"]
                .as_integer()
                .ok_or("Missing kline_his default_days")
                .unwrap_or_else(|_| 10i64)
                .to_owned(),
        })
    }
}

/// Returns a future that waits until the next scheduled time.
/// This future can be used directly in `select!` without spawning.
///
///  sec  min   hour   dayofmonth   month   dayofweek   year
pub async fn wait_until_next_scheduled_time(cron_expr: &str, message: &str) -> DateTime<Tz> {
    let schedule = Schedule::from_str(cron_expr).expect("Failed to parse CRON expression");

    let now = Utc::now().with_timezone(&chrono_tz::Asia::Shanghai);
    let (until_next, next_time) = if
        let Some(next) = schedule.upcoming(chrono_tz::Asia::Shanghai).take(1).next()
    {
        let until_next = (next - now).to_std().unwrap_or_default();
        let next_time = next;

        (until_next, next_time)
    } else {
        let until_next = std::time::Duration::from_secs(1);

        let next_time =
            Utc::now().with_timezone(&chrono_tz::Asia::Shanghai) + chrono::Duration::seconds(1);
        (until_next, next_time)
    };

    // Wait for the calculated duration.
    println!("({message}): {:?} ...", next_time);
    tokio::time::sleep(tokio::time::Duration::from(until_next)).await;

    next_time
}

pub fn register_ctrl_c() -> Arc<Mutex<mpsc::Receiver<()>>> {
    // Listen for Ctrl-C signal
    let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
    let shutdown_rx_arc = Arc::new(Mutex::new(shutdown_rx)); // Wrap the receiver in Arc<Mutex<_>>

    // Spawn a task that waits for the Ctrl-C signal and triggers shutdown
    tokio::spawn(async move {
        signal::ctrl_c().await.expect("Failed to install Ctrl+C handler");
        let _ = shutdown_tx.send(());
        info!("Received Ctrl-C, shutting down...");
    });
    info!("Press CTRL-C to quit...");

    shutdown_rx_arc
}

#[cfg(test)]
mod test {
    use chrono::Utc;

    use crate::cli::commands::wait_until_next_scheduled_time;

    #[tokio::test]
    async fn test_wait_until_next_scheduled_time() {
        let cron_expr = "0/5 * * * * *"; // Every 5 seconds
        for i in 0..10 {
            let start = Utc::now();
            let next_time = wait_until_next_scheduled_time(cron_expr, "test").await;
            let end = Utc::now();

            // Print the next time for debugging
            println!("Next[{i}] scheduled time: {:?} spent={:?}", next_time, end - start);
        }
    }
}
